/* Sentinel - IRC Statistical and Operator Services
** servers.c - Functions to track server state
**
** Copyright W. Campbell and others.  See README for more details
** Some code Copyright: Jonathan George, Kai Seidler, ircd-hybrid Team,
**                      IRCnet IRCD developers.
**
** $Id: servers.c 10 2010-02-02 01:12:49Z dubkat $
*/

#include <assert.h>
#include "stats.h"
#include "struct.h"
#include "proto.h"
#include "match.h"
#include "dlink.h"
#include "slog.h"
#include "mem.h"

static struct Server *new_server(char *, char *);
dlink_list serverhash[S_TABLE_SIZE];

static struct Server *new_server(char *name, char *desc)
{
  struct Server *s;
  dlink_node *dl = dlink_create();

  s = smalloc(sizeof(struct Server));
  if (!name)
    name = "";
  strlcpy(s->name, name, MAXHOST);
  if (!desc)
    desc = "";
#if defined(IRCD_HYBRID) || defined(IRCD_HYB6) || defined(IRCD_HYB7) || \
    defined(IRCD_HYBSVS) || defined(IRCD_RATBOX)
  /* Remove the (H) hidden identifier */
  if ( (desc[0] == '(') &&
       (desc[1] == 'H') &&
       (desc[2] == ')') )
    desc += 4;
#endif

  strlcpy(s->desc, desc, LINE);
  s->hash = HASH((unsigned char *) name, S_TABLE_SIZE);
  dlink_add(s, dl, &serverhash[s->hash]);

  s->invis = 0;

  debug("new_server: %s, %s", name, desc);

  return s;
}

struct Server *AddServer(char *name, int hops, int token, char *desc,
                               struct Server* uplink)
{
  struct Server *s;

#ifdef JUPESERV
#if defined(TRACK_ADMIN) || defined(TRACK_VERSION)

  struct Jupe *j = FindServerJupe(name);
#endif
#endif

  debug("New Server: %s", name);

  /* Check to see if a server already exists - Why is a mystery to me
  ** but sometimes on Hyb7 it'll happen
  */
  s = findserver(name);
  if (s)
  {
    slog(DEFAULT_LOG, L_WARNING, "Server %s already exists in the server table",
         name);
  }
  else
  {
    s = new_server(name, desc);
  }
  s->hops = hops;
  s->connected_since = CurrentTime;
  s->last_announce = CurrentTime;
  s->uplink = uplink; /* Ok if it's NULL */

  debug("findserver() --> %s uplink %s", s->name,
        s->uplink ? s->uplink->name : "NULL");

#ifdef TRACK_VERSION
  /* If it is NOT settings.name */
  if (strcasecmp(settings.name, name))
  {
#ifdef JUPESERV
    if (j == NULL)
#endif

      sts(":%s VERSION %s", AdminServ.nick, name);
  }
  else
  {
    snprintf(s->version, BUFSIZE, "%s %s -> %s",
             stats_version, VersionFlags(), stats_date);
  }
#endif

#ifdef TRACK_ADMIN
  /* If it is NOT settings.name */
  if (strcasecmp(settings.name, name))
  {
#ifdef JUPESERV
    if (j == NULL)
#endif

      sts(":%s ADMIN %s", AdminServ.nick, name);
    /* admin items do not need to be zero'ed, calloc'ed memory */
  }
  else
  {
    snprintf(s->admin1, BUFSIZE, "%s Network Statistical Services",
             settings.netname);
    snprintf(s->admin2, BUFSIZE, "%s", settings.admin_name);
    snprintf(s->admin3, BUFSIZE, "%s", settings.admin_email);
  }
#endif

  /* Check to see if it should be hidden */
  if (findhidden(name))
    s->flags |= SFLAG_HIDE;

  /* Check for the (H) in the server desc field, and mark the
  ** server as hidden.  We probably should not add it to the 
  ** Hidden list though, it'll be a waste
  */

#if defined(IRCD_HYBRID) || defined(IRCD_HYB6) || defined(IRCD_HYB7) || \
    defined(IRCD_HYBSVS) || defined(IRCD_RATBOX)
  /* Keep it enabled even if we are not linked to a Hybrid 7
  ** server, just in case there's a "hidden hub" on the network
  */
  if (desc)
    if ( (desc[0] == '(') &&
         (desc[1] == 'H') &&
         (desc[2] == ')') )
      s->flags |= SFLAG_HIDE;
#endif

  IncreaseServers();
  AddStats(s);

  return s;
}

/* This is similar to DelServer, but always recurses down the tree!
** This function is used when the connection to the uplink is lost
*/
void FlushServer(struct Server *s)
{
  struct Server *up;
  struct User* u;
  int i = 0;
  dlink_node *dl, *tdl;

  /* Should never happen */
  if (s == NULL)
    return ;

  debug("FlushServer called for %s", s->name);

  for (i = 0; i < U_TABLE_SIZE; i++)
  {
    DLINK_FOREACH_SAFE(dl, tdl, userhash[i].head)
    {
      u = dl->data;

      if (!strcmp(s->name, u->server->name))
        DelUserU(u);
    }
  }
  for (i = 0; i < S_TABLE_SIZE; i++)
  {
    DLINK_FOREACH_SAFE(dl, tdl, serverhash[i].head)
    {
      up = dl->data;

      if (up->uplink == NULL)
        continue;
      if (!strcmp(s->name, up->uplink->name))
        FlushServer(up);
    }
  }

#ifdef SPLITSERV
  SpDelServer(s->name);
  if (IsSPTrackOnDisconnect())
  {
    if ((s != me.s) && (!IsSPHide() || !(IsSPHide() && IsSHide(s))))
      SpNewSplit(s->name);
  }
#endif

  dl = dlink_find_delete(s, &serverhash[s->hash]);
  dlink_free(dl);
  s->stats->lastseen = CurrentTime;
  DecreaseServers();
  free(s);
}

void DelServer(char *name)
{
  struct Server *s = findserver(name);

  if (s == NULL)
  {
    slog(DEFAULT_LOG, L_WARNING, "DelServer(): %s failed!", name);
    return ;
  }
  DelServerS(s);
}

void DelServerS(struct Server *s)
{
#if defined(IRCD_HYB6) || defined(IRCD_HYB7) || defined(IRCD_BAHAMUT) || \
    defined(IRCD_HYBSVS) || defined(IRCD_RATBOX)
  /* This is to support the QS capab
  ** Bahamut does via NOQUIT
  */
  struct Server *up;
  struct User* u;
  int i = 0;
  dlink_node *dl, *tdl;
#endif

  if (s == NULL)
  {
    /* assert? */
    return ;
  }

  if (s->stats != NULL)
    s->stats->numsplits++;

#if defined(IRCD_HYB6) || defined(IRCD_HYB7) || defined(IRCD_BAHAMUT) || \
    defined(IRCD_HYBSVS) || defined(IRCD_RATBOX)

  for (i = 0; i < U_TABLE_SIZE; i++)
  {
    DLINK_FOREACH_SAFE(dl, tdl, userhash[i].head)
    {
      u = dl->data;

      if (!strcmp(s->name, u->server->name))
        DelUserU(u);
    }
  }
  for (i = 0; i < S_TABLE_SIZE; i++)
  {
    DLINK_FOREACH_SAFE(dl, tdl, serverhash[i].head)
    {
      up = dl->data;

      if (up->uplink == NULL)
        continue;
      if (!strcmp(s->name, up->uplink->name))
        DelServerS(up);
    }
  }
#endif

#ifdef SPLITSERV
  if (s != me.s)
  {
    slog(SPLIT_LOG, L_NOTICE, "Split: %s from %s", s->name,
         (s && (s->uplink)) ? s->uplink->name
         : "UNKNOWN");
    if (!is_delayed)
    {
      if (IsSPReport())
      {
        if (IsSPHide())
        {
          if (!IsSHide(s))
          {
            notice(sp.join_chan, SplitServ.nick, "SPLIT: %s from %s", s->name,
                    (s && s->uplink && IsSHide(s->uplink)) ? "HIDDEN" :
                    (s && (s->uplink)) ? s->uplink->name : "UNKNOWN");
          }
        }
        else
        {
          notice(sp.join_chan, SplitServ.nick, "SPLIT: %s from %s", s->name,
                  (s && (s->uplink)) ? s->uplink->name : "UNKNOWN");
        }
      }
    }

    SpDelServer(s->name);
    if (!IsSPHide() || !(IsSPHide() && IsSHide(s)))
      SpNewSplit(s->name);
  }
#endif

  dl = dlink_find_delete(s, &serverhash[s->hash]);
  dlink_free(dl);
  s->stats->lastseen = CurrentTime;
  DecreaseServers();
  assert(netinfo.servers >= 0);
  free(s);
}

struct Server *findserver(char *name)
{
  struct Server *s;
  dlink_node *dl;
  int i;
  unsigned long hv;

  hv = HASH((unsigned char *) name, S_TABLE_SIZE);

  debug("findserver(%s)", name);
  DLINK_FOREACH(dl, serverhash[hv].head)
  {
    s = dl->data;

    if (strcasecmp(s->name, name) == 0)
      return s;
  }
  /* masked servers, CPU hit */
  for (i = 0; i < S_TABLE_SIZE; i++)
  {
    DLINK_FOREACH(dl, serverhash[i].head)
    {
      s = dl->data;

      if (match(s->name, name) == 0)
        return s;
    }
  }

  return NULL;
}

void init_server_hash()
{
  int i;
  struct Server *s;
  dlink_node *dl, *tdl;

  for (i = 0; i < S_TABLE_SIZE; i++)
  {
    DLINK_FOREACH_SAFE(dl, tdl, serverhash[i].head)
    {
      s = dl->data;

      if (s != NULL)
      {
        debug("init_server_hash() deleting %s", s->name);
        DelServerS(s);
      }
    }
    memset(&serverhash[i], 0, sizeof(dlink_list));
  }

  s = AddServer(settings.name, 0, 0, settings.desc, NULL);
  me.s = s;
}

void IncreaseServers()
{
  debug("Increasing server count");
  netinfo.servers++;
  if (netinfo.servers > netinfo.maxservers)
  {
    netinfo.maxservers = netinfo.servers;
    netinfo.t_maxservers = CurrentTime;
  }
  if (netinfo.servers > daily.servers)
  {
    daily.servers = netinfo.servers;
    daily.t_servers = CurrentTime;
  }
}

